/*
 * PingApplication.java
 *
 * Created on April 22, 2007, 1:48 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.xeerkat.services.ping;

import com.sun.tools.javac.resources.version;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.logging.Level;
import org.infoset.xml.Document;
import org.infoset.xml.DocumentLoader;
import org.infoset.xml.Element;
import org.infoset.xml.Name;
import org.infoset.xml.XMLException;
import org.infoset.xml.sax.SAXDocumentLoader;
import org.restlet.Application;
import org.restlet.Client;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.Restlet;
import org.restlet.data.Form;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Protocol;
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.routing.Router;
import org.xeerkat.agent.AgentContext;
import org.xeerkat.agent.ResponseListener;
import org.xeerkat.agent.services.ServiceMetadata;
import org.xeerkat.xmpp.ID;

/**
 *
 * @author alex
 */
@ServiceMetadata(id="http://www.xeerkat.org/Services/Ping/2007/1/0",version="1.0",vendor="xeerkat.org",copyright="2008 xeerkat.org")
public class PingApplication extends Application
{
   
   static final Name PING_REQUEST = Name.create("request");

   static class Stat {
      int sent;
      int received;
      int requested;
      long start;
      long lastSent;
      long finished;
      Stat(int number) {
         start = System.currentTimeMillis();
         lastSent = start;
         sent = 0;
         received = 0;
         requested = number;
         finished = -1;
      }
   }
   
   AgentContext agentContext;
   /** Creates a new instance of PingApplication */
   public PingApplication(Context context)
   {
      super(context);
      agentContext = (AgentContext)context.getAttributes().get(AgentContext.AGENT_CONTEXT_ATTR);
   }
   
   public Restlet createRoot() {
      
      // Create a router for the root of the application
      Router router = new Router(getContext());
      
      // This is the main restlet that receives the requests for pings
      // It receives XML of the form:
      //    <request number="number" wait="time"/>
      // If the 'wait' attribute is omitted, it defaults to 3 minutes
      Restlet main = new Restlet(getContext()) {
         
         public void handle(Request request, Response response) {
            
            // Make sure we're responding to a post
            if (!request.getMethod().equals(Method.POST)) {
               response.setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED);
               return;
            }
            
            // Get the entity
            Representation rep = request.getEntity();
            if (rep==null) {
               response.setStatus(Status.CLIENT_ERROR_BAD_REQUEST,"No entity specified.");
               return;
            }
            
            // Make sure it is XML
            MediaType type = rep.getMediaType();
            if (!type.getName().equals(MediaType.APPLICATION_XML.getName()) && !type.getName().equals(MediaType.TEXT_XML.getName())) {
               response.setStatus(Status.CLIENT_ERROR_BAD_REQUEST,"Unsupport media type: "+type.getName());
               return;
            }
            
            // Get the charset for loading the XML
            String charset = rep.getMediaType().getParameters().getValues("charset");
            if (charset==null) {
               charset = "UTF-8";
            }
            
            try {
               
               
               // Load the XML
               DocumentLoader loader = new SAXDocumentLoader();
               Reader r = new InputStreamReader(rep.getStream(),charset);
               Document doc = loader.load(r);
               r.close();

               // the top element should be a 'request' element
               Element top = doc.getDocumentElement();
               
               if (top.getName().equals(PING_REQUEST)) {
                  
                  // get the number of pings (defaults to one)
                  String numberS = top.getAttributeValue("number");
                  int number = 1;
                  if (numberS!=null) {
                     number = Integer.parseInt(numberS);
                  }
                  
                  // get the maximum wait
                  long maxWait = ((long)Math.ceil(number/3.0))*60*1000;
                  if (maxWait==0) {
                     maxWait = 60000;
                  }
                  numberS = top.getAttributeValue("wait");
                  if (numberS!=null) {
                     maxWait = Long.parseLong(numberS);
                  }
                  
                  // our pause amount if we can't send pings
                  long tick = 50;
                  
                  getContext().getLogger().info("Ping request from "+request.getClientInfo().getAddress()+" for "+number+" pings.");
                  
                  final Stat stat = new Stat(number);
                  Client client = getContext().getClientDispatcher();
                  
                  // loop till we've timed out or received all the responses
                  while(stat.received<stat.requested && (System.currentTimeMillis()-stat.start)<maxWait) {
                     
                     // If there are no other agents, we can't ping anyone and so we'll pause
                     if (agentContext.getAgentMonitor().getAgents().size()==0) {
                        long amount = amount = 3*tick;
                        getLogger().info("No agents, waiting "+amount);
                        try {
                           Thread.currentThread().sleep(amount);
                        } catch (InterruptedException ex) {
                           ex.printStackTrace();
                        }
                     }
                     
                     int sent = 0;
                     int received = stat.received;
                     // Send pings to all agents only if we haven't done so in a little bit
                     if (stat.sent<stat.requested || (System.currentTimeMillis()-stat.lastSent)>200) {
                        for (ID id : agentContext.getAgentMonitor().getAgents()) {

                           // We're using the async protocol
                           // This get's us xeerkat://{self}/{id}/ping/echo in the correct format
                           Reference uri = agentContext.createRequestReference(AgentContext.ASYNC_PROTOCOL,id.toString(),"/ping/echo");
                           getLogger().info("Pinging "+uri);

                           // Make a GET request on the rest service for echo
                           Request pingRequest = new Request(Method.GET,uri);
                           Response pingResponse = client.handle(pingRequest);

                           // Success means we could send the request and not much more
                           if (pingResponse.getStatus().isSuccess()) {
                              sent++;

                              // Get the id and add a listener for the response
                              Form headers = (Form)pingResponse.getAttributes().get("org.restlet.http.headers");
                              String responseId = headers.getValues("x-response-id");
                              agentContext.registerResponseListener(responseId,new ResponseListener() {
                                 public void onResponse(String id,Response response) {
                                    stat.received++;

                                    // mark the moment we've satisfied the request
                                    if (stat.finished<0 && stat.received>=stat.requested) {
                                       stat.finished = System.currentTimeMillis();
                                    }
                                 }
                              });


                           } else {
                              getLogger().info("Cannot ping "+id+", status="+pingResponse.getStatus().getCode()+", "+(pingResponse.getEntity()==null ? "(no text)" : pingResponse.getEntity().getText()));
                           }
                           if (stat.sent<stat.received && (stat.sent+sent)==stat.received) {
                              // end the first time we hit our limit.
                              break;
                           }
                        }

                        stat.sent += sent;

                        if (sent>0) {
                           stat.lastSent = System.currentTimeMillis();
                        }

                     }
                     // If we couldn't send anything, pause
                     if (stat.received<stat.requested && (stat.received-received)==0 && sent==0 && agentContext.getAgentMonitor().getAgents().size()>0) {
                        try {
                           Thread.currentThread().sleep(tick);
                        } catch (InterruptedException ex) {
                           ex.printStackTrace();
                        }
                        getContext().getLogger().info("Tick: "+(System.currentTimeMillis()-stat.start)+", requested="+stat.requested+", sent="+stat.sent+", received="+stat.received);
                     }
                  }
                  if (stat.finished<0) {
                     stat.finished = System.currentTimeMillis();
                  }
                  
                  // We're done, send the response
                  long elapsed = System.currentTimeMillis()-stat.start;
                  long average = elapsed/stat.received;
                  response.setStatus(Status.SUCCESS_OK);
                  response.setEntity(new StringRepresentation("<stats requested='"+stat.requested+"' sent='"+stat.sent+"' received='"+stat.received+"' time='"+(stat.finished-stat.start)+"' elapsed='"+elapsed+"' average='"+average+"'/>",MediaType.APPLICATION_XML));
                  
               } else {
                  response.setStatus(Status.CLIENT_ERROR_BAD_REQUEST,"Unrecognized document element: "+top.getName());
               }
            } catch (XMLException ex) {
               response.setStatus(Status.CLIENT_ERROR_BAD_REQUEST,ex.getMessage());
            } catch (IOException ex) {
               getContext().getLogger().log(Level.SEVERE,"Cannot read input XML from entity.",ex);
            }
         }
      };
      router.attach("",main);
      router.attach("/",main);
      router.attach("/echo",new Restlet(getContext()) {
         public void handle(Request request, Response response) {
            response.setEntity(request.getEntity());
            response.setStatus(Status.SUCCESS_OK);
         }
      });
      router.attach("/log",new Restlet(getContext()) {
         public void handle(Request request, Response response) {
            getLogger().info(request.getEntityAsText());
            response.setStatus(Status.SUCCESS_NO_CONTENT);
         }
      });
      return router;
   }
   
}
